package edu.ubb.remeye.client.communication.handler;

import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

import org.apache.mina.core.session.IoSession;
import org.apache.mina.statemachine.StateControl;
import org.apache.mina.statemachine.annotation.IoHandlerTransition;
import org.apache.mina.statemachine.annotation.State;
import org.apache.mina.statemachine.context.AbstractStateContext;
import org.codecompany.jeha.ExceptionHandler;
import org.codecompany.jeha.core.HandlerUtil;

import edu.ubb.remeye.client.communication.RemEyeClient;
import edu.ubb.remeye.client.settings.ClientRuntimeSettings;
import edu.ubb.remeye.client.settings.ClientSetter;
import edu.ubb.remeye.communication.MessageType;
import edu.ubb.remeye.communication.message.Message;
import edu.ubb.remeye.control.Control;
import edu.ubb.remeye.encodings.EncodingType;
import edu.ubb.remeye.encodings.encoders.ImageDecoder;
import edu.ubb.remeye.encodings.encoders.bmp.BMPImageDecoder;
import edu.ubb.remeye.encodings.encoders.jpeg.JPEGImageDecoder;
import edu.ubb.remeye.encodings.encoders.png.PNGImageDecoder;
import edu.ubb.remeye.exception.ImageDecodingException;
import edu.ubb.remeye.exception.handler.RemEyeExceptionHandler;
import edu.ubb.remeye.gui.internationalization.LanguageManager;
import edu.ubb.remeye.security.Security;
import edu.ubb.remeye.security.ResultType;
import edu.ubb.remeye.security.SecurityType;
import edu.ubb.remeye.security.manager.none.ClientNoneAuthenticator;
import edu.ubb.remeye.security.manager.vnc.ClientVNCAuthenticator;

import static org.apache.mina.statemachine.event.IoHandlerEvents.*;

/**
 * Class implementing the communication protocol using Apache MINA State Machine and
 * Apache MINA IoHandler events.
 * 
 * @author Andrew Ianicicau
 *
 */
@ExceptionHandler(handler=RemEyeExceptionHandler.class)
public class ClientStateHandler {

	///////////////////////////////////////////////////////////////////////////////////
	////		ATTRIBUTES - ClientStateHandler
	///////////////////////////////////////////////////////////////////////////////////
	
	/**
	 * Client class object.
	 */
	private RemEyeClient client;
	
	/**
	 * Seconds to wait for session timeout.
	 */
	private static final int TIMEOUT_SECONDS = 60;
	

	
	///////////////////////////////////////////////////////////////////////////////////
	////		STATES - ClientStateHandler
	///////////////////////////////////////////////////////////////////////////////////
	
	/**
	 * Base state for unhandled events.
	 */
	@State
	public static final String BASE = "BASE";
	
	/**
	 * Identified.
	 */
	@State(BASE)
	public static final String IDENTIFIED = "IDENTIFIED";
	
	/**
	 * Connection established.
	 */
	@State(BASE)
	public static final String CONNECTED = "CONNECTED";
	
	/**
	 * Authentication in progress.
	 */
	@State(BASE)
	public static final String AUTHENTICATION = "AUTHENTICATION";
	
	/**
	 * Authentication finished.
	 */
	@State(BASE)
	public static final String AUTHENTICATED = "AUTHENTICATED";
	
	/**
	 * Set sharing settings.
	 */
	@State(BASE)
	public static final String SET = "SET";
	
	/**
	 * Control request.
	 */
	@State(BASE)
	public static final String CONTROL_REQUEST = "CONTROL_REQUEST";
	
	/**
	 * Desktop is shared.
	 */
	@State(BASE)
	public static final String SHARED = "SHARED";
	
	
	
	///////////////////////////////////////////////////////////////////////////////////
	////		CLASSES - ClientStateHandler
	///////////////////////////////////////////////////////////////////////////////////

	/**
	 * Class containing state and related attributes for client.
	 */
	public static class ClientContext extends AbstractStateContext {

		///////////////////////////////////////////////////////////////////////////////////
		////		ATTRIBUTES - ClientContext
		///////////////////////////////////////////////////////////////////////////////////
		
		/**
		 * Client identifier.
		 */
		private long clientId;
		
		/**
		 * Security type in use.
		 */
		private SecurityType security;
		
		/**
		 * Security manager.
		 */
		private Security securityManager;
		
		/**
		 * Shows if client is in control of server.
		 */
		private boolean control;
		
		/**
		 * Image decoder.
		 */
		private ImageDecoder imageDecoder;
		
		
		
		///////////////////////////////////////////////////////////////////////////////////
		////		CONSTRUCTORS - ClientContext
		///////////////////////////////////////////////////////////////////////////////////

		/**
		 * Default constructor.
		 */
		public ClientContext() {
			super();
		}
		
		
		
		///////////////////////////////////////////////////////////////////////////////////
		////		METHODS - ClientContext
		///////////////////////////////////////////////////////////////////////////////////
		
		public SecurityType getSecurity() {
			return security;
		}
		
		public void setSecurity(SecurityType security) {
			this.security = security;
		}
		
		public Security getSecurityManager() {
			return securityManager;
		}

		public void setSecurityManager(Security securityManager) {
			this.securityManager = securityManager;
		}

		public long getClientId() {
			return clientId;
		}

		public void setClientId(long clientId) {
			this.clientId = clientId;
		}
		
		public boolean isControl() {
			return control;
		}

		public void setControl(boolean control) {
			this.control = control;
		}

		public ImageDecoder getImageDecoder() {
			return imageDecoder;
		}
		
		public void setImageDecoder(ImageDecoder imageDecoder) {
			this.imageDecoder = imageDecoder;
		}
		
	}
	
	
	
	///////////////////////////////////////////////////////////////////////////////////
	////		CONSTRUCTORS - ClientStateHandler
	///////////////////////////////////////////////////////////////////////////////////
	
	public ClientStateHandler(RemEyeClient client) {
		this.client = client;
	}
	
	
	
	///////////////////////////////////////////////////////////////////////////////////
	////		METHODS - ClientStateHandler
	///////////////////////////////////////////////////////////////////////////////////
	
	/**
	 * Method for unhandled events.
	 */
	@IoHandlerTransition(in = BASE, weight = 100)
	public void unhandledEvent() {
		// nothing to do
	}
	
	/**
	 * Closes the session on session idle state considered as a timeout.
	 * @param context Client state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_IDLE, in = BASE, weight = 100)
	public void timeout(ClientContext context, IoSession session) {
		session.close(true);
		Control.showInformation(LanguageManager.getLanguageManager().getText("timeout"));
	}
	
	/**
	 * Called on SESSION_OPENED event in BASE state resulting a CONNECTED state.
	 * Takes the state machine from BASE to CONNECTED state and sets session timeout.
	 * @param context Client state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_OPENED, in = BASE, next = CONNECTED)
	public void sessionOpened(ClientContext context, IoSession session) {
		// setting session timeout
		session.getConfig().setBothIdleTime(TIMEOUT_SECONDS);
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in CONNECTED state resulting a IDENTIFIED state.
	 * Set the client identifier from received message.
	 * @param context Client state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = CONNECTED, next = IDENTIFIED)
	public void setIdentity(ClientContext context, IoSession session, Message message) {
		// check for valid message, must be IDENTIFIER
		if(message.getMessageType().equals(MessageType.IDENTIFIER)) {
			// set client identifier
			long id = message.getMessage()[0];
			context.setClientId(id);
		} else {
			StateControl.breakAndGotoNext(CONNECTED);
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in IDENTIFIED state resulting a AUTHENTICATION state.
	 * Get security list and select one if available and start authentication if valid 
	 * security type is found.
	 * @param context Client state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = IDENTIFIED, next = AUTHENTICATION)
	public void setSecurity(ClientContext context, IoSession session, Message message) {
		// check for valid message, must be SECURITY_LIST
		if(message.getMessageType().equals(MessageType.SECURITY_LIST)) {
			// select security from available security list received in message
			SecurityType selected = selectSecurity(message);
			
			// set security type
			context.setSecurity(selected);
			
			// send security type answer
			Message securityMessage = createSecurityMessage(selected);
			session.write(securityMessage);
			
			// check for valid security type
			if(!selected.equals(SecurityType.INVALID))
				// if security type is valid start authentication
				startAuthentication(context, session);
		} else {
			StateControl.breakAndGotoNext(IDENTIFIED);
		}	
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in AUTHENTICATION state resulting the same state.
	 * If authentication finished sets AUTHENTICATED state else forwards the received messages 
	 * to security manager thread.
	 * @param context Client state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = AUTHENTICATION, next = AUTHENTICATION)
	public void authenticationMessage(ClientContext context, IoSession session, Message message) {
		// check for authentication finished
		if(authenticated(session)) {
			StateControl.breakAndCallNow(AUTHENTICATED);
		} else {
			// forward message to security manager thread
			// check for valid message, must be AUTHENTICATION
			if(message.getMessageType().equals(MessageType.AUTHENTICATION)) {
				context.getSecurityManager().forwardMessage(message);
			}
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in AUTHENTICATED state resulting a SET state.
	 * Gets the authentication result. If succeeded sends an encoding message.
	 * @param context Client state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = AUTHENTICATED, next = SET)
	public void authenticationResult(ClientContext context, IoSession session, Message message) {
		// check for valid message, must be AUTHENTICATION_RESULT
		if(message.getMessageType().equals(MessageType.AUTHENTICATION_RESULT)) {
			if(getAuthenticationResult(message)) {
				Control.showInformation(LanguageManager.getLanguageManager().getText("authenticationsuccess"));
				
				// send encoding message
				EncodingType desired = ClientSetter.getInstance().getDesiredEncodingType();
				Message encodingMessage = createEncodingMessage(desired);
				session.write(encodingMessage);
			} else {
				Control.showInformation(LanguageManager.getLanguageManager().getText("authenticationfailed"));
			}
		} else {
			StateControl.breakAndGotoNext(AUTHENTICATED);
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in SET state resulting a CONTROL_REQUEST state.
	 * Gets the encoding selection result and after that sends a control request message.
	 * @param context Client state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = SET, next = CONTROL_REQUEST)
	public void setEncodingResult(ClientContext context, IoSession session, Message message) {
		// check for valid message, must be ENCODING_ANSWER
		if(message.getMessageType().equals(MessageType.ENCODING_ANSWER)) {
			// set encoding
			int ordinal = message.getMessage()[0];
			EncodingType selected = EncodingType.getEncodingType(ordinal);
			context.setImageDecoder(getImageDecoder(selected));
			
			// send control request message
			boolean getControl = ClientSetter.getInstance().getControl();
			Message controlMessage = createControlMessage(getControl);
			session.write(controlMessage);
		} else {
			StateControl.breakAndGotoNext(SET);
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in CONTROL_REQUEST state resulting a SHARED state.
	 * Gets the control request result and sends an UPDATE message for screen image data. 
	 * If control allowed, initializes the control handler.
	 * @param context Client state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = CONTROL_REQUEST, next = SHARED)
	public void controlRequestResult(ClientContext context, IoSession session, Message message) {
		// check for valid message, must be CONTROL_ANSWER
		if(message.getMessageType().equals(MessageType.CONTROL_ANSWER)) {
			if(getControlAnswer(message, context)) {
				ClientRuntimeSettings.getInstance().setClientSession(session);
				Control.addEventListeners();
				Control.showInformation(LanguageManager.getLanguageManager().getText("controlok"));
			} else {
				if(ClientSetter.getInstance().getControl())
					Control.showInformation(LanguageManager.getLanguageManager().getText("controlnotok"));
			}
			
			// request update
			Message updateMessage = new Message(MessageType.UPDATE, new byte[0]);
			session.write(updateMessage);
		} else {
			StateControl.breakAndGotoNext(CONTROL_REQUEST);
		}
	}
	
	/**
	 * Called on MESSAGE_RECEIVED event in SHARED state resulting a SHARED state.
	 * Receives the image data, updates the viewer with it and sends an other update request.
	 * @param context Client state context.
	 * @param session Actual client session.
	 * @param message Message received.
	 */
	@IoHandlerTransition(on = MESSAGE_RECEIVED, in = SHARED, next = SHARED)
	public void receiveEvent(ClientContext context, IoSession session, Message message) {	
		// check for valid message, must be DATA
		if(message.getMessageType().equals(MessageType.DATA)) {
			
			try {
				BufferedImage image = (BufferedImage) context.getImageDecoder().decode(message.getMessage());
				Control.drawImage(image);
			} catch(ImageDecodingException e) {
				HandlerUtil.handle(e);
			}
			
			// request update
			Message updateMessage = new Message(MessageType.UPDATE, new byte[0]);
			session.write(updateMessage);
		}
	}
	
	/**
	 * Called on SESSION_CLOSED event in BASE.
	 * Stops the client.
	 * @param context Client state context.
	 * @param session Actual client session.
	 */
	@IoHandlerTransition(on = SESSION_CLOSED, in = BASE)
	public void sessionClosed(ClientContext context, IoSession session) {
		Control.removeEventListeners();
		client.disconnectClient();
	}
	
	/**
	 * Select the security type to use according to desired security if it is available in
	 * security type list received in message.
	 * @param message SECURITY_LIST message.
	 * @return Selected security type.
	 */
	private SecurityType selectSecurity(Message message) {
		List<SecurityType> list = new ArrayList<SecurityType>();
		byte[] securityTypes = message.getMessage();
		for(int i = 0; i < securityTypes.length; i++) {
			list.add(SecurityType.getSecurityType(securityTypes[i]));
		}
		SecurityType desired = ClientSetter.getInstance().getDesiredSecurityType();
		return SecurityType.listContains(list, desired);
	}
	
	/**
	 * Returns a message of type SECURITY_ANSWER containing the selected security type.
	 * @param type Security type to put into message.
	 * @return SECURITY_ANSWER message.
	 */
	private Message createSecurityMessage(SecurityType type) {
		byte[] security = new byte[1];
		security[0] = (byte) type.ordinal();
		Message securityMessage = new Message(MessageType.SECURITY_ANSWER, security);
		return securityMessage;
	}
	
	/**
	 * Returns the client security manager to use, according to security type.
	 * @param type Security type to use.
	 * @param session Actual client session.
	 * @return Security manager.
	 */
	private Security getSecurityManager(SecurityType type, IoSession session) {
		switch(type) {
		case NONE:
			return new ClientNoneAuthenticator(session);
		case VNC:
			return new ClientVNCAuthenticator(session);
		default:
			return null;
		}
	}
	
	/**
	 * Returns the state of authentication.
	 * @param session Actual client session.
	 * @return True if authentication finished, else false.
	 */
	private boolean authenticated(IoSession session) {
		if(session.getAttribute("authenticated") != null)
			return true;
		return false;
	}
	
	/**
	 * Starts the authentication thread using the right security manager.
	 * @param context Client state context.
	 * @param session Actual client session.
	 */
	private void startAuthentication(ClientContext context, IoSession session) {
		Security manager = getSecurityManager(context.getSecurity(), session);
		context.setSecurityManager(manager);
		Thread securityThread = new Thread(context.getSecurityManager());
		securityThread.start();
	}
	
	/**
	 * Returns the result of authentication.
	 * @param message AUTHENTICATION_RESULT message.
	 * @return True if succeeded, else false. 
	 */
	private boolean getAuthenticationResult(Message message) {
		byte ordinal = message.getMessage()[0];
		ResultType result = ResultType.getResultType(ordinal);
		if(result.equals(ResultType.SUCCEEDED))
			return true;
		return false;
	}
	
	/**
	 * Returns a message of type ENCODING containing the encoding type.
	 * @param type Encoding type.
	 * @return ENCODING message.
	 */
	private Message createEncodingMessage(EncodingType type) {
		byte[] encoding = new byte[1];
		encoding[0] = (byte) type.ordinal();
		Message encodingMessage = new Message(MessageType.ENCODING, encoding);
		return encodingMessage;
	}
	
	/**
	 * Returns a message of type CONTROL_REQUEST containing the control request boolean.
	 * @param control The control request boolean.
	 * @return CONTROL_REQUEST message.
	 */
	private Message createControlMessage(boolean controlRequest) {
		byte[] control = new byte[1];
		if(controlRequest)
			control[0] = Byte.MAX_VALUE;
		Message controlMessage = new Message(MessageType.CONTROL_REQUEST, control);
		return controlMessage;
	}
	
	/**
	 * Returns the result of the control request.
	 * @param message CONTROL_ANSWER message.
	 * @param context Client state context.
	 * @return True if control is allowed, else false.
	 */
	private boolean getControlAnswer(Message message, ClientContext context) {
		boolean control = false;
		if(message.getMessage()[0] > 0)
			control = true;
		context.setControl(control);
		return control;
	}
	
	/**
	 * Return the image decoder.
	 * @param encoding The selected decoder type.
	 * @return Image decoder or null.
	 */
	private ImageDecoder getImageDecoder(EncodingType encoding) {
		switch(encoding) {
		case BMP:
			return new BMPImageDecoder();
		case JPEG:
			return new JPEGImageDecoder();
		case PNG:
			return new PNGImageDecoder();
		default:
			return null;
		}
	}

}
